<?php

namespace paper\dao;

use http\Exception\RuntimeException;
use paper\exception\DbException;

class Query extends Builder
{
    private Connection $unsafeConnection;
    private Builder $builder;
    private ?string $index = null; //索引字段
    private array $with = [];

    public function __construct(Connection $connection)
    {
        $this->unsafeConnection = $connection;
        $this->builder = $this;
    }

    /**
     * 开启事务
     * @noinspection PhpUnused
     */
    public function startTrans()
    {
        $this->getSafeConnection()->startTrans();
    }


    /**
     * @return Connection
     */
    public function getSafeConnection(): Connection
    {
        if(!$this->unsafeConnection->isConnected())
            $this->unsafeConnection->connect();
        return $this->unsafeConnection;
    }

    /**
     * 回滚事务
     * @noinspection PhpUnused
     */
    public function rollback()
    {
        $this->getSafeConnection()->rollBack();
    }

    /**
     * 回滚事务
     * @noinspection PhpUnused
     */
    public function commit()
    {
        $this->getSafeConnection()->commit();
    }

    public function inTransaction(): bool
    {
        return $this->getSafeConnection()->inTransaction();
    }


    public function index($filed = 'id'): static
    {
        $this->index = $filed;
        return $this;
    }

    /**
     * @param $limit
     * @param null $page
     * @param bool|int $max
     * @return Paginate
     * @throws DbException
     */
    public function paginate($limit, $page = null, bool|int $max = false): Paginate
    {
        //保存原字段
        $save_order = $this->builder->options['order'];
        $save_field = $this->builder->options['field'];
        unset($this->builder->options['order']);
//        var_dump($this->builder->builder->options);exit;
        $this->limit($max); //清除限制条件
        $rows = $this->count("*");
        //恢复字段
        $this->builder->options['order'] = $save_order;
        $this->builder->options['field'] = $save_field;
        if ($page == null) {
            $page = request()->get('page', 1);
        }
        $paginate = new Paginate($rows, $page, $limit);
        $paginate->setQuery($this);
        return $paginate;
    }

    /**
     * @return bool|array
     * @throws DbException
     */
    public function find(): bool|array
    {
        if (!$this->options['limit'])
            $this->limit(1);
        return $this->fetch($this->builder->buildSelectSql(),
            $this->builder->fetchBindData());
    }

    /**
     * @return array
     * @throws DbException
     */
    public function select(): array
    {
        return $this->fetchAll($this->builder->buildSelectSql(),
            $this->builder->fetchBindData());
    }

    /**
     * @return bool|int
     * @throws DbException
     */
    public function delete(): bool|int
    {
        return $this->getSafeConnection()->execute($this->builder->buildDeleteSql(),
            $this->builder->fetchBindData());
    }

    /**
     * @param array $data
     * @return bool|int
     * @throws DbException
     */
    public function update(array $data = []): bool|int
    {
        return $this->getSafeConnection()->execute($this->builder->buildUpdateSql($data),
            $this->builder->fetchBindData());
    }

    /**
     * @param string $field
     * @return int
     * @throws DbException
     */
    public function count(string $field = "*"): int
    {
        return $this->aggregate('count', $field);
    }

    /**
     * @param string $field
     * @return int
     * @throws DbException
     */
    public function sum(string $field): int
    {
        return $this->aggregate('sum', $field);
    }

    /**
     * @param string $field
     * @return int
     * @throws DbException
     */
    public function max(string $field): int
    {
        return $this->aggregate('max', $field);
    }

    /**
     * @param string $field
     * @return int
     * @throws DbException
     */
    public function min(string $field): int
    {
        return $this->aggregate('min', $field);
    }

    /**
     * @param $aggregate
     * @param string $field
     * @return int
     * @throws DbException
     */
    public function aggregate($aggregate, string $field = "*"): int
    {
        $result = $this->getSafeConnection()->fetch($this->builder->buildAggregateSql($aggregate, $field), $this->builder->fetchBindData());
        if ($result) {
            return $result["_AGGREGATE_"] ?? 0;
        }
        return 0;
    }

    /**
     * 插入数据
     * @param $data
     * @return bool
     * @throws DbException
     */
    public function insert($data): bool
    {
        return $this->getSafeConnection()->execute($this->builder->buildInsertSql($data), $this->builder->fetchBindData());
    }

    /**
     * 插入数据并获取最后一条的自增ID
     * @param array $data
     * @return bool|string
     * @throws DbException
     */
    function insertGetLastId(array $data = []): bool|string
    {
        if ($this->insert($data)) {
            return $this->getLastId();
        }
        return false;
    }

    /**
     * 获取最后一次插入的ID
     * @return string
     */
    public function getLastId(): string
    {
        return $this->getSafeConnection()->lastInsertId();
    }

    /**
     * @param $data
     * @return bool|int
     * @throws DbException
     */
    public function insertAll($data): bool|int
    {
        if (!$data || count($data) == 0)
            throw new RuntimeException("data must not be empty array!");
        $firstData = $data[0];
        $count = 0;
        $sql = $this->builder->buildInsertSql($firstData);
        $bindData = $this->builder->fetchBindData();
        $fieldKeys = array_keys($bindData);
        $connection = $this->getSafeConnection();
        $statement = $connection->prepare($sql);
        foreach ($data as $item) {
            $ins = array_combine($fieldKeys, $item);
            if ($connection->execute($statement, $ins)) {
                $count++;
            }
        }
        return $count;

    }

    public function lockTable()
    {

    }

    /**
     * @param $sql
     * @param array|null $data
     * @return array|bool
     * @throws DbException
     */
    private function fetch($sql, ?array $data = []): bool|array
    {
        return $this->getSafeConnection()->fetch($sql, $data);
    }


    /**
     * @param      $sql
     * @param array|null $data
     * @return array
     * @throws DbException
     */
    private function fetchAll($sql, ?array $data = []): array
    {
        if (!$results = $this->getSafeConnection()->fetchAll($sql, $data)) {
            return [];
        }
        $resultArr = [];
        if ($this->index) {
            foreach ($results as $item)
                $resultArr[$item[$this->index]] = $item;
        } else {
            $resultArr = $results;
        }
        if (count($this->with) > 0 && $resultArr) {
            foreach ($this->with as $with) {
                $table = $with['table'];
                $localKey = $with['localKey'];
                $foreignKey = $with['foreignKey'];
                $field = $with['field'];
                $where = $with['where'];
                $in = array_column($resultArr, $localKey);
                $withResult = (new self($this->getSafeConnection()))
                    ->name($table)
                    ->field($field)
                    ->where($foreignKey, 'in', $in)
                    ->where($where)
                    ->index($foreignKey)
                    ->select();
                foreach ($resultArr as &$item) {
                    $item[$table] = $withResult[$item[$localKey]] ?? [];
                }
            }
        }
        return $resultArr;
    }

    /**
     * @param $table
     * @param $localKey
     * @param $foreignKey
     * @param string $field
     * @param array $where
     * @return $this
     */
    public function with($table, $localKey, $foreignKey, string $field = "*", array $where = []): static
    {
        $this->with[] = [
            'table' => $table,
            'localKey' => $localKey,
            'foreignKey' => $foreignKey,
            'field' => $field,
            'where' => $where,
        ];
        return $this;
    }

}